﻿using LanguagePolish.Controls.WeekCalendar;
using LanguagePolish.EntityModel;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;

namespace LanguagePolish.Controls
{
    /// <summary>
    /// Логика взаимодействия для WeekGrid.xaml
    /// </summary>
    public partial class WeekGrid : UserControl
    {
        private class LessonsPositionWrapper
        {
            public Lesson Lesson { get; set; }

            public TimeSpan TimeFrom 
            { 
                get { return Lesson.TimeStart.Value; } 
            }

            public TimeSpan TimeTo
            {
                get { return Lesson.TimeEnd.Value; }
            }

            public bool IsPlaced { get; set; }
            public double PosFrom { get; set; }
            public double PosTo { get; set; }

            public LessonsPositionWrapper(Lesson lesson)
            {
                Lesson = lesson;
            }
        }

        private class PlaceRange
        {
            public double From { get; set; }
            public double To { get; set; }

            public List<LessonsPositionWrapper> Lessons = new List<LessonsPositionWrapper>();
        }

        public static readonly DependencyProperty LessonsProperty =
            DependencyProperty.Register("Lessons", typeof(IEnumerable<Lesson>),
            typeof(WeekGrid), new PropertyMetadata(null, OnLessonsChanged));

        public static readonly DependencyProperty CurrentDateProperty =
            DependencyProperty.Register("CurrentDate", typeof(DateTime),
            typeof(WeekGrid), new PropertyMetadata(DateTime.Now, OnCurrentDateChanged));

        public static readonly DependencyProperty EnableMenuProperty =
            DependencyProperty.Register("EnableMenu", typeof(bool), typeof(WeekGrid), new PropertyMetadata(true, OnEnableMenuChanged));

        private static void OnEnableMenuChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            WeekGrid g = (WeekGrid)d;
            g.Init();
        }

        public bool EnableMenu
        {
            get { return (bool)GetValue(EnableMenuProperty); }
            set { SetValue(EnableMenuProperty, value); }
        }

        private static void OnCurrentDateChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            WeekGrid g = (WeekGrid)d;
            g.InitDays();
            g.InitLessons();
        }

        private static void OnLessonsChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            WeekGrid g = (WeekGrid)d;
            g.InitLessons();
        }

        public IEnumerable<Lesson> Lessons
        {
            get { return (IEnumerable<Lesson>)this.GetValue(LessonsProperty); }
            set
            {
                this.SetValue(LessonsProperty, value);
                InitLessons();
            }
        }

        public event EventHandler<AddLessonEventArgs> AddLesson;

        public event EventHandler<EditLessonEventArgs> EditLesson;

        public event EventHandler<RemoveLessonEventArgs> RemoveLesson;

        private Canvas[] canvases = new Canvas[8];
        private Label[] dayLabels = new Label[7];

        const double startOffset = 8;
        const double endOffset = 8;

        private int minHours = 8;
        public int MinHours
        {
            get { return minHours; }
            set { minHours = value; }
        }

        private int maxHours = 22;
        public int MaxHours
        {
            get { return maxHours; }
            set { maxHours = value; }
        }

        public DateTime CurrentDate
        {
            get { return (DateTime)GetValue(CurrentDateProperty); }
            set 
            {
                SetValue(CurrentDateProperty, value);
                InitDays();
            }
        }

        /// <summary>
        /// Первый день текущей недели.
        /// </summary>
        public DateTime FirstDay
        {
            get
            {
                int day = CurrentDate.DayOfWeek.ToInt();
                return CurrentDate.AddDays(-day);
            }
        }

        /// <summary>
        /// Последний день текущей недели.
        /// </summary>
        public DateTime LastDay
        {
            get
            {
                int day = CurrentDate.DayOfWeek.ToInt();
                return CurrentDate.AddDays(6 - day);
            }
        }

        public WeekGrid()
        {
            InitializeComponent();
            Init();
        }

        private void Init()
        {
            grdMain.RowDefinitions.Clear();
            grdMain.ColumnDefinitions.Clear();
            grdMain.Children.Clear();
            InitRowsColumns();
            InitCanvas();
            InitHours();
            InitDayLabels();
            InitDays();
        }

        private void InitRowsColumns()
        {
            grdMain.RowDefinitions.Add(new RowDefinition { Height = new GridLength(0, GridUnitType.Auto) });
            grdMain.RowDefinitions.Add(new RowDefinition());

            grdMain.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(30) });
            for (int i = 0; i < 7; i++)
                grdMain.ColumnDefinitions.Add(new ColumnDefinition());
        }

        private void InitCanvas()
        {
            for (int i = 0; i < 8; i++)
            {
                Border bord = new Border();
                if (i > 0 && EnableMenu)
                {
                    var cm = new System.Windows.Controls.ContextMenu();
                    var mi = new MenuItem();
                    mi.Tag = i - 1;       // день недели
                    mi.Header = "Add Lesson";
                    mi.Click += mi_Click;
                    cm.Items.Add(mi);
                    bord.ContextMenu = cm;
                }
                bord.BorderBrush = Brushes.Black;
                bord.BorderThickness = new Thickness(1);
                Grid.SetColumn(bord, i);
                Grid.SetRow(bord, 1);
                grdMain.Children.Add(bord);

                Canvas cnvs = new Canvas();
                canvases[i] = cnvs;
                cnvs.Background = Brushes.WhiteSmoke;
                cnvs.HorizontalAlignment = System.Windows.HorizontalAlignment.Stretch;
                cnvs.VerticalAlignment = System.Windows.VerticalAlignment.Stretch;
                bord.Child = cnvs;
                //Grid.SetColumn(cnvs, i);
                //Grid.SetRow(cnvs, 1);
                //grdMain.Children.Add(cnvs);
            }
        }

        void mi_Click(object sender, RoutedEventArgs e)
        {
            var s = (FrameworkElement)sender;
            int datNum = (int)s.Tag;
            DateTime date = FirstDay.AddDays(datNum);
            if (AddLesson != null)
                AddLesson(this, new AddLessonEventArgs(date));
        }

        private void InitDayLabels()
        {
            for (int i = 0; i < 7; i++)
            {
                Label lblDay = dayLabels[i] = new Label();
                lblDay.HorizontalAlignment = System.Windows.HorizontalAlignment.Center;
                Grid.SetColumn(lblDay, i + 1);
                Grid.SetRow(lblDay, 0);
                grdMain.Children.Add(lblDay);
            }
        }

        private void InitDays()
        {
            int firstDay = (int)CurrentDate.DayOfWeek;
            firstDay = firstDay == 0 ? 6 : firstDay - 1;
            DateTime date = CurrentDate.AddDays(-firstDay);
            for (int i = 0; i < 7; i++)
            {
                Label lblDay = dayLabels[i];
                string dayName = GlobalSettings.DaysOfWeek[i];
                string month = GlobalSettings.MonthsShort[date.Month - 1];
                string lblContent = string.Format("{0}, {1} {2}", dayName, month, date.Day);
                lblDay.Content = lblContent;
                date = date.AddDays(1);
            }
        }

        private void InitHours()
        {
            Canvas cnvs = canvases[0];
            cnvs.Children.Clear();

            double hourStep = (cnvs.ActualHeight - startOffset - endOffset) / (MaxHours - MinHours);
            if (cnvs.ActualHeight == 0)
                hourStep = 10;
            double hourPosY = startOffset;
            for (int i = MinHours; i <= MaxHours; i++)
            {
                Label lbl = new Label();
                lbl.Margin = new Thickness(2, 0, 0, 0);
                lbl.Padding = new Thickness(0);
                lbl.Content = i;
                cnvs.Children.Add(lbl);
                double offset = 8;
                Canvas.SetLeft(lbl, 0);
                Canvas.SetTop(lbl, hourPosY - offset);

                //LINE
                Line myLine = new Line();
                myLine.Stroke = System.Windows.Media.Brushes.Black;

                myLine.X1 = 20;
                myLine.X2 = 40; 
                myLine.Y1 = hourPosY;
                myLine.Y2 = hourPosY;
                myLine.SnapsToDevicePixels = true;
                myLine.SetValue(RenderOptions.EdgeModeProperty, EdgeMode.Aliased);
                myLine.StrokeThickness = 1;
                cnvs.Children.Add(myLine);

                hourPosY += hourStep;
            }
        }

        #region Lessons Placement

        /// <summary>
        /// Сепер охрененный и в тоже время элегантный алгоритм распихивания уроков по времени в расписание.
        /// </summary>
        private void InitLessons()
        {
            ResetCanvases();
            if (Lessons == null || !Lessons.Any())
                return;
            // перебираем все дни
            for (int i = 0; i < 7; i++)
            {
                DateTime currentDay = FirstDay.AddDays(i);
                var dayLessons = Lessons.Where(l => l.LessonDate.HasValue && l.LessonDate.Value.Date == currentDay.Date).ToList();
                if (dayLessons.Count == 0)
                    continue;
                List<LessonsPositionWrapper> processedLessons = RepackLessons(dayLessons);
                List<List<int>> boundaryGroups = CreateBoundaryGroups(processedLessons);
                List<int> biggestGroups = FindBiggestGroup(processedLessons, boundaryGroups);
                List<int> groupsToProcess = GetGroupsToProcess(boundaryGroups, biggestGroups);
                ProcessLessonsGroups(processedLessons, boundaryGroups, groupsToProcess);
                PlaceLessons(processedLessons, i);
            }
        }

        /// <summary>
        /// Очищает полотно для рисования.
        /// </summary>
        private void ResetCanvases()
        {
            for (int i = 1; i < 8; i++)
            {
                canvases[i].Children.Clear();
            }
        }

        /// <summary>
        /// Перепаковывает коллекцию уроков в коллекцию классов со специальной инфой.
        /// </summary>
        /// <param name="lessons"></param>
        /// <returns></returns>
        private List<LessonsPositionWrapper> RepackLessons(IEnumerable<Lesson> lessons)
        {
            List<LessonsPositionWrapper> result = new List<LessonsPositionWrapper>();
            foreach (var less in lessons)
            {
                // только если время определено
                if (less.TimeStart.HasValue && less.TimeEnd.HasValue)
                {
                    result.Add(new LessonsPositionWrapper(less));
                }
            }
            return result;
        }

        /// <summary>
        /// Записывает какие уроки лежат в каждой точке начала и конца каждого урока.
        /// </summary>
        /// <param name="processedLessons"></param>
        /// <returns></returns>
        private List<List<int>> CreateBoundaryGroups(List<LessonsPositionWrapper> processedLessons)
        {
            List<List<int>> result = new List<List<int>>();
            foreach (LessonsPositionWrapper Lesson in processedLessons)
            {
                List<int> groupStart = new List<int>();
                List<int> groupEnd = new List<int>();
                TimeSpan from = Lesson.TimeFrom;
                TimeSpan to = Lesson.TimeTo;
                for (int i = 0; i < processedLessons.Count; i++)
                {
                    LessonsPositionWrapper sch = processedLessons[i];
                    if (sch.TimeFrom <= from && from < sch.TimeTo)
                        groupStart.Add(i);
                    if (sch.TimeFrom <= to && to < sch.TimeTo)
                        groupEnd.Add(i);
                }
                result.Add(groupStart);
                result.Add(groupEnd);
            }
            return result;
        }
        
        /// <summary>
        /// Определяет для каждого урока находит группы в которых он встречается наибольшее кол-во раз.
        /// </summary>
        /// <param name="lessons"></param>
        /// <param name="boundaryGroups"></param>
        /// <returns></returns>
        private List<int> FindBiggestGroup(List<LessonsPositionWrapper> lessons, List<List<int>> boundaryGroups)
        {
            List<int> result = new List<int>();
            for (int i = 0; i < lessons.Count; i++)
            {
                int bigGroup = 0;
                int maxElems = 0;
                for (int j = 0; j < boundaryGroups.Count; j++)
                {
                    if (boundaryGroups[j].Count > maxElems && boundaryGroups[j].Contains(i))
                    {
                        maxElems = boundaryGroups[i].Count;
                        bigGroup = j;
                    }
                }
                result.Add(bigGroup);
            }
            return result.Distinct().ToList();  // без повторений
        }
        
        /// <summary>
        /// Сортирует группы с предыдущего этапа по кол-ву уроков в них от наибольших.
        /// </summary>
        /// <param name="boundaryGroups"></param>
        /// <param name="biggestGroups"></param>
        /// <returns></returns>
        private List<int> GetGroupsToProcess(List<List<int>> boundaryGroups, List<int> biggestGroups)
        {
            List<int> result = biggestGroups.OrderByDescending(g => boundaryGroups[g].Count).ToList();
            return result;
        }

        /// <summary>
        /// Непосредственно производит распихивание уроков в относительных координатах.
        /// </summary>
        /// <param name="lessons"></param>
        /// <param name="boundaryGroups"></param>
        /// <param name="groupsToProcess"></param>
        private void ProcessLessonsGroups(List<LessonsPositionWrapper> lessons, List<List<int>> boundaryGroups, List<int> groupsToProcess)
        {
            for (int i = 0; i < groupsToProcess.Count; i++)
            {
                List<int> currentGroup = boundaryGroups[groupsToProcess[i]];

                List<LessonsPositionWrapper> placed = currentGroup.Select(n => lessons[n]).Where(s => s.IsPlaced).ToList();
                List<LessonsPositionWrapper> notPlaced = currentGroup.Select(n => lessons[n]).Where(s => !s.IsPlaced).ToList();

                List<PlaceRange> emptyPlaces = GetEmptyPlaces(placed);
                int placeNum = 0;
                for (int j = 0; j < notPlaced.Count; j++)
                {
                    emptyPlaces[placeNum].Lessons.Add(notPlaced[j]);
                    placeNum++;
                    placeNum %= emptyPlaces.Count;
                }
                foreach (PlaceRange place in emptyPlaces)
                {
                    double range = place.To - place.From;
                    double step = range / place.Lessons.Count;
                    double start = place.From;
                    foreach (LessonsPositionWrapper sch in place.Lessons)
                    {
                        sch.IsPlaced = true;
                        sch.PosFrom = start;
                        sch.PosTo = start + step;
                        start += step;
                    }
                }
            }
        }

        /// <summary>
        /// Возвращает список с пустыми местами выкидывая уже занятые.
        /// </summary>
        /// <param name="placed"></param>
        /// <returns></returns>
        List<PlaceRange> GetEmptyPlaces(List<LessonsPositionWrapper> placed)
        {
            if (placed == null || placed.Count == 0)
                return new List<PlaceRange> { new PlaceRange { From = 0, To = 1 } };
            placed = placed.OrderBy(p => p.PosFrom).ToList();
            List<PlaceRange> res = new List<PlaceRange>();
            PlaceRange current = new PlaceRange();
            for (int i = 0; i < placed.Count; i++)
            {
                current.To = placed[i].PosFrom;
                res.Add(current);
                current = new PlaceRange();
                current.From = placed[i].PosTo;
            }
            // последний кусочек
            current.To = 1;
            res.Add(current);
            res.RemoveAll(p => p.From.EqualWithEps(p.To));  // удаляем позиции с нулевой шириной.
            return res;
        }
        
        /// <summary>
        /// Производит позиционирование уроков.
        /// </summary>
        /// <param name="processedLessons"></param>
        /// <param name="day"></param>
        private void PlaceLessons(List<LessonsPositionWrapper> processedLessons, int day)
        {
            Canvas canv = canvases[day + 1];
            if (canv.ActualHeight == 0 || canv.ActualWidth == 0)
                return;
            double hourStep = (canv.ActualHeight - startOffset - endOffset) / (MaxHours - MinHours);
            double columnWidth = canv.ActualWidth;
            foreach (LessonsPositionWrapper les in processedLessons)
            {
                WeekCalendarCell cell = new WeekCalendarCell();
                cell.EnableMenu = EnableMenu;
                cell.AddLesson += cell_AddLesson;
                cell.RemoveLesson += cell_RemoveLesson;
                cell.EditLesson += cell_EditLesson;
                cell.Date = les.Lesson.LessonDate.Value.Date;
                cell.DataContext = les.Lesson;
                double y1 = (les.TimeFrom.TotalHours - MinHours) * hourStep + startOffset;
                double y2 = (les.TimeTo.TotalHours - MinHours) * hourStep + startOffset;
                double height = y2 - y1;

                double x1 = les.PosFrom * columnWidth;
                double x2 = les.PosTo * columnWidth;
                double width = x2 - x1;
                cell.Height = height;
                cell.Width = width;

                Canvas.SetLeft(cell, x1);
                Canvas.SetTop(cell, y1);
                canv.Children.Add(cell);
            }
        }

        #endregion

        void cell_EditLesson(object sender, EditLessonEventArgs e)
        {
            if (EditLesson != null)
                EditLesson(this, e);
        }

        void cell_RemoveLesson(object sender, RemoveLessonEventArgs e)
        {
            if (RemoveLesson != null)
                RemoveLesson(this, e);
        }

        void cell_AddLesson(object sender, AddLessonEventArgs e)
        {
            if (AddLesson != null)
                AddLesson(this, e);
        }

        private void grdMain_SizeChanged(object sender, SizeChangedEventArgs e)
        {
            InitHours();
            InitLessons();
        }

    }
}
